TutorialApp Hosting

How to Deploy Any App with a Dockerfile

Chajio Cloud recognises Node.js, Python and many other stacks on its own. When yours isn’t one of them, or you want full control over how your app is built, add a Dockerfile to your repository and the platform builds exactly what it describes. This guide covers the rules a Dockerfile needs to follow here, with complete examples for Go and Laravel.

CChajio Cloud team September 25, 2026 7 min read
On this page

When to use a Dockerfile

  • Your language or framework isn’t detected automatically: Go, Rust, .NET, Java, PHP frameworks and more.
  • Your app needs system packages, such as image or PDF tools, installed alongside it.
  • You already build a Docker image for your app, and want production to use the same one.

There is no setting to change. If your repository has a file named Dockerfile in its root, or in the folder you set as the build context, the platform builds from it instead of detecting your setup. Everything else in the deploy works the same, as described in the guides for Node.js and Python.

Five rules for a Dockerfile that deploys cleanly

1. Listen on PORT, on every interface

When your app runs, the PORT environment variable holds the port it must listen on, matching the Container port setting (3000 unless you change it). Listen on 0.0.0.0, not localhost, or your app starts but can’t be reached. If your server can’t read PORT, set Container port to the port it uses instead. EXPOSE lines are fine to keep, but they don’t change anything here.

2. Answer the health check

New versions only receive visitors once the Health check path (/ by default) answers with a success response. Point it at a route that answers quickly without a login, such as /health.

3. Declare build-time variables with ARG

Every variable on your Environment tab reaches your app when it runs. To use one while the image is built, for example a public API address compiled into a frontend, declare it with an ARG line. Each ARG receives the variable with the same name, and the RUN commands after it can read it. Variables you don’t declare never reach the build.

Keep secrets out of ARG

Values used during the build are recorded in the image. Secrets such as database passwords or API keys don’t need an ARG: read them when your app runs, which keeps them out of the image entirely. The same goes for .env files: don’t copy one into the image.

4. Run one process in the foreground

Start your server with an exec-form CMD, such as CMD ["./server"], and keep it in the foreground. When you deploy, the old version gets a SIGTERM signal and a short time to finish the requests it’s handling; exec form makes sure that signal reaches your app rather than a shell. If you need $PORT in the command line itself, use CMD ["sh", "-c", "exec your-server --port $PORT"].

5. Keep the image lean

Build in one stage and copy only the result into a small final stage, as in the examples below. Smaller images build and roll out faster. Add a .dockerignore so local clutter never reaches the build:

.dockerignore
.git
node_modules
.env
*.log

Running as a non-root user, with a USER line, is a good habit too.

Example: a Go web server

main.go
package main

import (
	"log"
	"net/http"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello from Chajio Cloud"))
	})
	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ok"))
	})

	log.Printf("listening on :%s", port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}
Dockerfile
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server .

FROM alpine:3.20
RUN adduser -D app
USER app
COPY --from=build /server /server
CMD ["/server"]

":" + port listens on every interface. Set the health check path to /health. The final image holds a single binary, so it’s a few megabytes.

Example: Laravel

This uses the official PHP image with Apache, which reads its port from PORT when it starts:

Dockerfile
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction

FROM php:8.3-apache
RUN docker-php-ext-install pdo_mysql \
 && a2enmod rewrite \
 && sed -i 's/Listen 80/Listen ${PORT}/' /etc/apache2/ports.conf \
 && sed -i 's/<VirtualHost \*:80>/<VirtualHost *:${PORT}>/' /etc/apache2/sites-available/000-default.conf \
 && sed -i 's#/var/www/html#/var/www/html/public#' /etc/apache2/sites-available/000-default.conf \
 && sed -i 's/AllowOverride None/AllowOverride All/' /etc/apache2/apache2.conf
ENV PORT=3000
WORKDIR /var/www/html
COPY . .
COPY --from=vendor /app/vendor ./vendor
RUN chown -R www-data:www-data storage bootstrap/cache

The last sed lets Laravel’s .htaccess route every URL through index.php. If your app builds its CSS and JavaScript with Vite, add a Node.js stage that runs npm ci && npm run build and copy its public/build folder into the final image.

Then, on the dashboard:

  1. Set the Health check path to /up, the health route built into Laravel 11 and later.
  2. Add APP_KEY (from php artisan key:generate --show), APP_ENV=production and APP_URL on the Environment tab.
  3. For a database, create a MariaDB database and name its app variable DB_URL, which Laravel reads directly, and set DB_CONNECTION=mariadb.
  4. So that Laravel builds https:// links, trust the platform’s proxy in bootstrap/app.php: ->withMiddleware(fn ($middleware) => $middleware->trustProxies(at: '*')).

Uploaded files written to storage/ aren’t shared between the two copies of your app, and disappear on the next deploy. Store uploads on an external disk, such as S3-compatible object storage.

Test it on your computer first

Building and running your image locally catches most problems in seconds instead of minutes. With Docker installed:

docker build -t myapp .
docker run --rm -e PORT=3000 -p 3000:3000 myapp

# in another terminal
curl http://localhost:3000/health

If that answers, it will answer on Chajio Cloud too. Pass build-time variables locally with --build-arg NAME=value.

Common problems

The build log warns about a “missing build argument”

Your Dockerfile declares an ARG that has no variable of the same name on the Environment tab, so it’s empty during the build. Add the variable, then redeploy. The build log lists the variables it received and any it asked for but didn’t get.

The app “did not become healthy”

Check the Logs tab. Usually the server listens on localhost or a fixed port, or the health check path returns an error. Test with the local docker run above.

COPY fails with “not found”

Paths in a Dockerfile are relative to the build context. If your app is in a subfolder, set Build context to that folder and keep the Dockerfile inside it. Also check the file isn’t excluded by .dockerignore or .gitignore: only files in your repository are available.

Keep reading

Ready to deploy?

Start on the Free plan: your first month is on us, and every app runs two copies for high availability.

View App Hosting plans