Skip to content

Docker

Deploy можно. with Docker Compose. One command to get the server, PostgreSQL database, and all dependencies running.

Prerequisites

  • Docker 24+ and Docker Compose v2
  • At least 1 GB of available RAM

Quick Start

Create a docker-compose.yml file and run:

bash
docker compose up -d

The web dashboard will be available at http://localhost:8080.

docker-compose.yml

yaml

services:
  postgres:
    image: postgres:15-alpine
    container_name: mozhno-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: feature_flags
      POSTGRES_USER: flags_user
      POSTGRES_PASSWORD: ${DB_PASSWORD:-flags_password}
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U flags_user -d feature_flags"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    deploy:
      resources:
        limits:
          memory: 512M
    networks:
      - mozhno-net

  mozhno:
    image: mozhnodev/mozhno:latest
    container_name: mozhno-server
    restart: unless-stopped
    ports:
      - "8080:8080"
    user: "1000:1000"
    read_only: true
    tmpfs:
      - /tmp:size=128M
    environment:
      MOZHNO_DB_URL: jdbc:postgresql://postgres:5432/feature_flags
      MOZHNO_DB_USERNAME: flags_user
      MOZHNO_DB_PASSWORD: ${DB_PASSWORD:-flags_password}

      MOZHNO_JWT_SECRET: ${MOZHNO_JWT_SECRET:-}
      MOZHNO_JWT_ACCESS_TOKEN_TTL_MINUTES: "15"
      MOZHNO_JWT_REFRESH_TOKEN_TTL_DAYS: "30"

      MOZHNO_SERVER_PORT: "8080"
      MOZHNO_DB_POOL_MAX_SIZE: "30"
      MOZHNO_DB_POOL_MIN_IDLE: "5"
      MOZHNO_CACHE_TTL_MINUTES: "5"

      MOZHNO_INIT_EMAIL: ${MOZHNO_INIT_EMAIL:-}
      MOZHNO_INIT_PASSWORD: ${MOZHNO_INIT_PASSWORD:-}

      JAVA_TOOL_OPTIONS: >
        -XX:+UseZGC
        -XX:MaxRAMPercentage=75.0
        -XX:+ExitOnOutOfMemoryError
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/actuator/health"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 60s
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: "2"
        reservations:
          memory: 512M
          cpus: "0.25"
    networks:
      - mozhno-net

volumes:
  pgdata:

networks:
  mozhno-net:
    driver: bridge

Environment Variables

Database

VariableRequiredDefaultDescription
MOZHNO_DB_URLNojdbc:postgresql://localhost:5432/feature_flagsJDBC URL for PostgreSQL
MOZHNO_DB_USERNAMENoflags_userDatabase user
MOZHNO_DB_PASSWORDNoflags_passwordDatabase password
MOZHNO_DB_POOL_MAX_SIZENo20Maximum database connections
MOZHNO_DB_POOL_MIN_IDLENo5Minimum idle database connections

JWT

VariableRequiredDefaultDescription
MOZHNO_JWT_SECRETNo— (optional for dev)HMAC-SHA256 signing secret. Minimum 32 bytes. Auto-generated if not set — tokens are invalidated on restart. Set explicitly in production.
MOZHNO_JWT_ACCESS_TOKEN_TTL_MINUTESNo15Access token lifetime in minutes
MOZHNO_JWT_REFRESH_TOKEN_TTL_DAYSNo30Refresh token lifetime in days

Server

VariableRequiredDefaultDescription
MOZHNO_SERVER_PORTNo8080HTTP port

JVM

VariableRequiredDefaultDescription
JAVA_TOOL_OPTIONSNoSee belowJVM arguments

JVM defaults:

  • -XX:+UseZGC — Z Garbage Collector for low-latency pause times
  • -XX:MaxRAMPercentage=75.0 — Max heap at 75% of container memory limit
  • -XX:+ExitOnOutOfMemoryError — Fail fast on OOM instead of hanging
  • -Djava.security.egd=file:/dev/./urandom — Faster random number generation

Optional

VariableRequiredDefaultDescription
LOGGING_LEVEL_DEV_MOZHNONoINFOApplication log level (DEBUG, INFO, WARN, ERROR)
MOZHNO_FLYWAY_ENABLEDNotrueRun Flyway migrations on startup
MOZHNO_CACHE_TTL_MINUTESNo5In-memory cache TTL in minutes
MOZHNO_CLIENT_MAX_METRICS_PER_KEYNo1000Max stored metrics per client key
MOZHNO_BASE_URLNohttp://localhost:8080Public base URL of the server

Bootstrap (First Launch)

On first launch with an empty database, the server bootstraps:

VariableRequiredDefaultDescription
MOZHNO_INIT_EMAILNoEmail of the admin user created on first launch. If not set, no admin is created.
MOZHNO_INIT_PASSWORDNoPassword for the initial admin user.
  • An admin user — when both variables are set and no users exist in the DB
  • A default project named «Default Project» — when no projects exist in the DB

If the variables are not set, the server starts with no users. Login will be impossible. Set them in .env or environment for your first launch.

Health Checks

The server exposes health endpoints via Spring Boot Actuator:

EndpointPurpose
/actuator/healthOverall health status (DB, disk space)
/actuator/health/livenessApplication liveness — is the JVM running?
/actuator/health/readinessApplication readiness — can it serve traffic?

Docker health check uses /actuator/health to detect unhealthy containers.

Volumes

VolumePurpose
pgdataPostgreSQL data directory. Persists across container restarts.

Resource Limits

ServiceCPU LimitMemory LimitCPU ReservationMemory Reservation
mozhno2 cores2 GB0.25 cores512 MB
postgres512 MB

Adjust these based on your workload. Memory reservations ensure the scheduler places containers on nodes with sufficient resources. CPU reservations control minimum CPU shares.

Network Configuration

All services communicate over the mozhno-net bridge network. PostgreSQL is not exposed to the host — only the Mozhno server port 8080 is published.

For production:

  • Place the stack behind a reverse proxy (nginx, Traefik, Caddy) for TLS termination
  • Use an external PostgreSQL if you already run a managed database
  • Bind the server port to 127.0.0.1 if using a reverse proxy on the same host:
yaml
ports:
  - "127.0.0.1:8080:8080"

Security Considerations

Non-Root User

The container runs as user 1000:1000 (named mozhno), not as root. This limits the impact of a container escape.

Read-Only Filesystem

The container filesystem is mounted read-only (read_only: true). A writable /tmp is provided as a tmpfs volume (128 MB in memory) for temporary files required by the JVM.

Secrets Management

Never hardcode secrets in docker-compose.yml. Use:

Environment file (.env):

bash
DB_PASSWORD=your-secure-database-password
MOZHNO_JWT_SECRET=your-strong-secret-string

Docker secrets (Swarm mode):

yaml
secrets:
  db_password:
    external: true
  jwt_secret:
    external: true

Environment variable reference in docker-compose.yml:

yaml
environment:
  MOZHNO_DB_PASSWORD: ${DB_PASSWORD}
  MOZHNO_JWT_SECRET: ${MOZHNO_JWT_SECRET}

Production Checklist

  • [ ] Generate a strong MOZHNO_JWT_SECRET (>= 32 characters plain text, or Base64-encoded)
  • [ ] Use a unique, random DB_PASSWORD
  • [ ] Place behind a reverse proxy with TLS (Let's Encrypt)
  • [ ] Bind to 127.0.0.1 if proxy is local
  • [ ] Set up database backups (see Database)
  • [ ] Configure resource limits appropriate for your workload
  • [ ] Pin Docker image tag to a specific version (avoid latest in production)
  • [ ] Enable Docker log rotation to prevent disk exhaustion

Dockerfile Multi-Stage Build

The official image is built in three stages:

StageBase ImagePurpose
web-buildernode:24-alpineBuilds React 19 SPA with Vite
java-buildereclipse-temurin:25-jdk-alpineCompiles Spring Boot application with Gradle
runtimeeclipse-temurin:25-jre-nobleMinimal runtime with JRE only

The resulting image contains only the JRE and the pre-built static resources embedded in the server JAR — no JDK, no Node.js, no build toolchain.

Upgrading

bash
# 1. Update the image tag in docker-compose.yml
#    image: mozhnodev/mozhno:v1.1.0

# 2. Pull the new image and restart
docker compose pull mozhno
docker compose up -d mozhno

# 3. Flyway automatically applies new migrations on startup

The process is safe: the old container runs until the new one is ready. The health check ensures traffic is routed only after a successful start.

Rolling Back

bash
# Revert to the old tag in docker-compose.yml
docker compose pull mozhno
docker compose up -d mozhno

Flyway migrations are not automatically rolled back. If the new version added migrations, rolling back the code is safe (migrations are forward-compatible).

Reverse Proxy & TLS

In production, always place можно. behind a reverse proxy with HTTPS.

Nginx

nginx
server {
    listen 443 ssl http2;
    server_name flags.example.com;

    ssl_certificate     /etc/letsencrypt/live/flags.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/flags.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

In docker-compose.yml, bind the port to localhost only:

yaml
ports:
  - '127.0.0.1:8080:8080'

And set MOZHNO_BASE_URL to your domain:

yaml
MOZHNO_BASE_URL: https://flags.example.com

Caddy (Automatic TLS)

flags.example.com {
    reverse_proxy localhost:8080
}

Production Checklist

#ActionCommand / Variable
0Create admin (first launch)MOZHNO_INIT_EMAIL, MOZHNO_INIT_PASSWORD → change password after login
1Generate JWT secretopenssl rand -base64 32 (one option; plain text >= 32 chars also works) → MOZHNO_JWT_SECRET
2Strong database passwordMOZHNO_DB_PASSWORD
3Set public domainMOZHNO_BASE_URL=https://flags.example.com
4Configure CORSMOZHNO_SECURITY_CORS_ALLOWED_ORIGINS=https://app.example.com
5Bind port to localhost onlyports: ['127.0.0.1:8080:8080']
6Set up TLS via Nginx/Caddy/TraefikSee section above
7Increase connection poolMOZHNO_DB_POOL_MAX_SIZE=30
8Configure SMTP for emailsMOZHNO_SMTP_HOST, MOZHNO_SMTP_PORT, MOZHNO_SMTP_USERNAME, MOZHNO_SMTP_PASSWORD
9Pin the image versionimage: mozhnodev/mozhno:v1.0.0
10Set up PostgreSQL backupspg_dump or WAL archiving, see Database
11Set up monitoringPrometheus, alerts — see Monitoring

Released under the BSL 1.1 License.