docker-compose-prod.yml
Rama main · réplica de lectura del repositorio original.
services:
database:
image: mysql:8.4.11
container_name: cloudcodex-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASS}
env_file:
- .env
# Published so a mysql client or `npm run migrate` on this host can reach
# it (docs/deployment.md), and on 127.0.0.1 only unless DB_BIND says
# otherwise: Docker's published ports are a DNAT rule in front of the host
# firewall, so a mapping with no host address puts the database on every
# network the machine is on, past `ufw deny`. The app does not use this; it
# reaches MySQL as DB_HOST: database over the compose network.
# docker-compose-release.yml publishes no database port at all.
ports:
- "${DB_BIND:-127.0.0.1}:3306:3306"
volumes:
- db_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro,z
# Over TCP, not the socket. On an empty volume the image first applies
# init.sql on a temporary server with networking off, and a socket ping
# answers that server: the app then started while nothing listened on
# 3306 and exited on ECONNREFUSED until the restart policy recovered it.
# Nothing answers 127.0.0.1 until the real server is up. start_period
# covers the initialisation, so a slow first boot is not marked unhealthy:
# the app waits on service_healthy, and an unhealthy database would stop
# `up` outright. A passing ping ends the start period at once, so a
# generous value costs a normal boot nothing.
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--protocol=tcp", "-u${DB_USER}", "-p${DB_PASS}"]
interval: 7s
timeout: 8s
retries: 15
start_period: 300s
app:
build:
context: ./cloudcodex
dockerfile: Dockerfile
container_name: cloudcodex-app
restart: unless-stopped
# Longer than the app's own ten-second shutdown bound (services/shutdown.js),
# so it flushes open documents and exits before Docker's SIGKILL. Docker's
# default is ten seconds, which would tie with it.
stop_grace_period: 20s
depends_on:
database:
condition: service_healthy
# Both sides follow PORT so setting it in .env moves the published mapping
# too. Without this the app would bind PORT inside the container while
# Compose still published 3000, and every request to the host would be
# refused while the logs showed a clean boot. `:-3000` and `:-127.0.0.1`
# cover the blank PORT= and APP_BIND= that .env.example ships.
#
# The host side is 127.0.0.1 unless APP_BIND says otherwise, so only this
# machine reaches the app: a browser on it for an evaluation, or the
# TLS-terminating reverse proxy production belongs behind. Docker's
# published ports are a DNAT rule in front of the host firewall, so a
# mapping with no host address is reachable from anywhere that can route
# here, past `ufw deny`. Set APP_BIND (0.0.0.0, or one interface's IPv4
# address) only to expose the port on purpose; see "TLS and reverse proxy"
# in docs/deployment.md. Never `::` or an IPv6 address: the network below
# is IPv4-only, so Docker would carry every IPv6 client in as its gateway,
# which TRUST_PROXY trusts.
ports:
- "${APP_BIND:-127.0.0.1}:${PORT:-3000}:${PORT:-3000}"
env_file:
- .env
environment:
DB_HOST: database
# The image runs `node server.js` directly, so NODE_ENV comes from its ENV
# alone, and a blank `NODE_ENV=` in .env (older .env.example files ship
# one) would replace it with an empty value. environment wins over env_file.
NODE_ENV: production
working_dir: /app
volumes:
# Same reason as docker-compose-release.yml: uploaded avatars and images
# extracted out of documents exist only on this disk, because
# routes/helpers/images.js swaps the base64 data URI in html_content for a
# /doc-images/ URL. Rebuilding without this volume loses all of them.
- app_public:/app/public
# The migration runner reads this. scripts/migrate.js ships in the image
# (it is inside the cloudcodex/ build context) but migrations/ is at the
# repo root and is not, so without this mount `npm run migrate` fails on a
# missing directory. /migrations is where scripts/migrate.js resolves it
# from WORKDIR /app.
#
# `ro,z`: read-only, because the runner never writes here, and `z` because
# on an SELinux host (Fedora, RHEL, CentOS, Rocky) a bind mount with no
# relabel flag is unreadable inside the container. Measured there:
# docker run --rm -v ./migrations:/migrations:ro node:20-slim ls /migrations
# ls: cannot open directory '/migrations': Permission denied
# Lowercase `z` (shared label), not `Z` (private): more than one container
# reads this directory, and `Z` would relabel it for one of them only.
# docker-compose.linux.yml adds :Z for the DEV file's mounts; it does not
# cover this file, so the flag belongs inline here.
- ./migrations:/migrations:ro,z
volumes:
db_data:
app_public:
# The project's default network, pinned so its gateway is an address
# TRUST_PROXY's default names (172.29.0.1/32). A proxy on this host reaches the
# app through the published port, and Docker presents that traffic to the
# container as this gateway. Left to Docker, the subnet is whatever is free
# (172.18.0.0/16 on a fresh host, a 192.168 range once about fifteen networks
# exist), the gateway is not in the default, and every client behind the proxy
# would share one rate-limit bucket. 172.29.0.0/16 is distinct from Cloud
# 172.28.0.0/16, a subnet a sibling deployment on the same host may use, so both can run there. An install created
# before this was pinned has its network on another subnet, and its upgrade
# must take the project `down` (never `down -v`) BEFORE migrating: stopping
# only the app and then migrating makes Compose replace this network under a
# running database, which it reconnects without its `database` alias, so the
# migration and the app both fail to find it. See "Upgrades" in
# docs/deployment.md.
networks:
default:
ipam:
config:
- subnet: 172.29.0.0/16
gateway: 172.29.0.1