# Dockerizing a Project for com1 Hosting

## Context

You are helping a user dockerize their project so it can be deployed on the com1 hosting platform. The platform uses:

- **Podman** (Docker-compatible) as the container runtime on the server
- **Podman Quadlet** `.container` units managed by `systemd --user` to run containers
- **Nginx** as a reverse proxy on the host for SSL termination and domain routing
- A shared **bridge network** (`com1_network`) connecting all containers in a stack
- **Environment variables** configured at the project level (injected into all containers in the project)

## How Deployment Works

1. The user defines a **Stack** (a logical group of projects)
2. Each stack has one or more **Projects** (e.g., "My Blog", "API Service")
3. Each project has one or more **Containers** (e.g., `web`, `worker`, `redis`)
4. Each project has **environment variables** shared across all its containers
5. At deploy time, the agent pulls each image, writes a Podman Quadlet `.container` unit per container, and starts it via `systemctl --user`
6. Nginx config is generated for any verified domains pointing to containers

## Dockerfile Requirements

Write a multi-stage Dockerfile. The platform runs on **ARM64/aarch64** (AWS Graviton) but may also run amd64. Follow these rules:

1. **Multi-stage build**: Use a builder stage for compilation/dependencies and a minimal runner stage for the final image.

2. **Layer caching**: Copy dependency/lock files first, install dependencies, then copy source code. This ensures dependency layers are cached when only source code changes.

3. **Minimal runner image**: Use a slim base image (e.g., `debian:bookworm-slim`, `alpine`, or the language's slim variant). Only install runtime dependencies, not build tools.

4. **Non-root user**: Run the application as a non-root user (e.g., `nobody` or create a dedicated user).

5. **Expose the correct port**: The container must listen on a known port. The default assumption is **port 80** - if no explicit port mappings are configured, the platform auto-assigns a host port mapping to container port 80. If the app listens on a different port, the user must configure an explicit port mapping.

6. **Health check friendly**: The app should respond to HTTP health checks. Expose a `GET /health` or `GET /healthz` endpoint that returns 200 OK.

7. **Environment variable driven**: All configuration (database URLs, API keys, secrets) must come from environment variables, NOT baked into the image. The platform injects env vars at runtime from the project's env var configuration.

8. **No hardcoded hostnames**: Other services in the stack are reachable via their compose service name on `com1_network`. Service names follow the pattern `{project_name}_{container_name}` (lowercased, non-alphanumeric replaced with underscores). For example, if the project is "My App" and the container is "postgres", the hostname is `my_app_postgres`.

9. **Volumes for persistent data**: If the app needs persistent storage (database data, uploads, etc.), document which paths should be mounted as volumes. Use named volumes (e.g., `pgdata:/var/lib/postgresql/data`) rather than host bind mounts when possible.

10. **Signal handling**: The entrypoint process must handle SIGTERM gracefully for clean container shutdown.

## Compose Integration Details

The generated compose file will include:

- **`image`**: `{image}:{tag}` from the container config (default tag: `latest`)
- **`networks`**: `["com1_network"]` (unless `network_mode` is set, which removes networks)
- **`container_name`**: `{project_name}_{container_name}` (normalized)
- **`ports`**: Either explicit mappings (`host:container/protocol`) or auto-assigned (`10000+id:80`)
- **`environment`**: Key-value map from the project's env vars
- **`volumes`**: `host_path:container_path[:ro]`
- **`healthcheck`**: `CMD-SHELL` with configurable interval/timeout/retries/start_period
- **`deploy.resources.limits`**: Memory (e.g., `512m`) and CPU (e.g., `0.5`) limits
- **`depends_on`**: Service dependency names for startup ordering
- **`command`**, **`entrypoint`**, **`working_dir`**, **`user`**, **`privileged`**, **`extra_hosts`**: Optional overrides

## Nginx Routing

Nginx runs on the host (not in a container) and routes traffic to containers:

- Each verified domain gets an nginx server block
- HTTP (port 80) redirects to HTTPS (port 443) with ACME challenge passthrough
- HTTPS terminates SSL and proxies to `127.0.0.1:{host_port}`
- WebSocket upgrade headers are included (`Upgrade`, `Connection`)
- Standard proxy headers are set: `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`

**Implication for the app**: The app receives traffic over plain HTTP from nginx. It should trust `X-Forwarded-*` headers for determining the client's real IP and protocol. Do NOT configure SSL/TLS in the app itself.

## Language-Specific Guidance

When dockerizing, apply these patterns based on the project's language/framework:

### Node.js / Next.js / Express

- Builder: `node:22-slim`, Runner: `node:22-slim` or `alpine`
- Copy `package.json` + `package-lock.json` first, then `npm ci --production`
- For Next.js: use `next build` with `output: 'standalone'` in `next.config.js`
- Expose port 3000 (default) - user must map host port to 3000

### Python / Django / FastAPI

- Builder: `python:3.13-slim`, Runner: same
- Copy `requirements.txt` first, then `pip install --no-cache-dir -r requirements.txt`
- Use `gunicorn` or `uvicorn` as the production server, not the dev server
- Expose port 8000 (typical)

### Go

- Builder: `golang:1.24`, Runner: `alpine` or `scratch`
- Copy `go.mod` + `go.sum` first, then `go mod download`
- Build with `CGO_ENABLED=0 go build -o /app`
- Extremely minimal runner image possible (`scratch` + ca-certificates)

### Ruby / Rails

- Builder: `ruby:3.4-slim`, Runner: same
- Copy `Gemfile` + `Gemfile.lock` first, then `bundle install`
- Precompile assets: `bundle exec rails assets:precompile`
- Use `puma` as the production server

### Elixir / Phoenix

- Builder: `elixir:1.19-slim`, Runner: `debian:trixie-slim`
- Copy `mix.exs` + `mix.lock` first, then `mix deps.get --only prod`
- Build OTP release: `MIX_ENV=prod mix release`
- Install esbuild for asset compilation if using Phoenix
- Runner needs: `libstdc++6`, `openssl`, `libncurses6`, `locales`

### PHP / Laravel

- Builder stage 1: `composer:2` for PHP dependencies
- Builder stage 2: `node:22-slim` for frontend assets (if using Vite/Mix)
- Runner: `php:{version}-fpm-bookworm` with nginx + supervisor
- Use **supervisord** to manage both nginx and php-fpm as a single container entrypoint
- Expose port 80 (nginx listens on 80, proxies to php-fpm via Unix socket)

**Critical — runtime directory for the PHP-FPM socket:**
The `/run` directory is a tmpfs that is freshly mounted when the container starts, so any directories created under `/run` during the Docker build (e.g. `mkdir -p /run/php`) will not exist at runtime. PHP-FPM will fail to create its Unix socket and nginx will return 502.

Fix: create the directory at container startup, not build time. In `supervisord.conf`, prepend the directory creation to the php-fpm command:

```ini
[program:php-fpm]
command=bash -c "mkdir -p /run/php && chown appuser:appgroup /run/php && /usr/local/sbin/php-fpm --nodaemonize"
```

**Critical — remove `zz-docker.conf`:**
The official `php-fpm` Docker image ships `/usr/local/etc/php-fpm.d/zz-docker.conf` which overrides `listen` to TCP port 9000. Since filenames are loaded alphabetically and `zz-` sorts last, it silently wins over your `www.conf`. PHP-FPM ends up listening on 9000 instead of the Unix socket, and nginx returns 502.

Fix: delete it in the Dockerfile after copying your pool config:

```dockerfile
COPY docker/www.conf /usr/local/etc/php-fpm.d/www.conf
RUN rm /usr/local/etc/php-fpm.d/zz-docker.conf
```

**Critical — nginx must be in the php-fpm socket group:**
The php-fpm socket is created with `listen.owner`/`listen.group` from `www.conf` (e.g. `appuser:appgroup`, mode `0660`). Nginx worker processes run as `www-data` by default and cannot access the socket, causing 502.

Fix: add `www-data` to the app group in the Dockerfile:

```dockerfile
RUN groupadd --system --gid 1001 appgroup &&     useradd --system --uid 1001 --gid appgroup appuser &&     usermod -aG appgroup www-data
```

**Laravel startup commands:**
Run these as a one-shot supervisord program (priority 1, before php-fpm and nginx) to handle migrations, caching, and seeding on every container start. Do NOT run `npm ci` / `npm run build` here — those are build-time steps handled in the Dockerfile frontend stage.

```ini
[program:init]
command=bash -c "cd /var/www && chown -R appuser:appgroup storage bootstrap/cache && php artisan optimize && php artisan storage:link && php artisan migrate --force && php artisan db:seed --force"
autostart=true
autorestart=false
startsecs=0
priority=1
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
```

Note: `php artisan optimize` already includes `config:cache`, `route:cache`, and `view:cache` — do not add `config:cache` separately.

The `chown -R appuser:appgroup storage bootstrap/cache` is needed because `/run` and storage permissions set during the Docker build may not survive correctly into the running container.

### Static sites (React, Vue, Hugo, etc.)

- Build stage produces static files
- Runner: `nginx:alpine` or `caddy:alpine` serving the static output
- Copy build output to `/usr/share/nginx/html` or equivalent
- Expose port 80

## Example: Dockerizing a Next.js App

```dockerfile
FROM node:22-slim AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]
```

Container configuration on com1:
- **Image**: the built image pushed to a registry
- **Port mapping**: `{host_port}:3000/tcp`
- **Env vars**: `DATABASE_URL`, `NEXTAUTH_SECRET`, etc. at the project level
- **Health check**: `curl -sf http://localhost:3000/api/health`

## Checklist Before Deployment

1. Image builds successfully with `docker build .`
2. App starts and listens on the expected port
3. All config comes from environment variables
4. A health endpoint exists and returns 200
5. The app handles `X-Forwarded-*` headers correctly (it's behind nginx)
6. Persistent data paths are documented for volume mounting
7. The image is pushed to a registry accessible by the server (Docker Hub, GHCR, etc.)
8. Inter-service communication uses compose service names, not `localhost`
9. Runtime directories (e.g. sockets, PIDs) are created at container startup, not in the Dockerfile — `/run` is a tmpfs and is reset on every container start
