
Volunteer migration of the editorial management system of the Revista de Direito do Consumidor — the journal of BRASILCON, the Brazilian Institute of Consumer Policy and Law — to a self-hosted Open Journal Systems (OJS) platform, now live at revistabrasilcon.com. I architected it, run it in production solo, and BRASILCON formally recognized the work in an official letter (Ofício nº 13/2026) signed by its President, the journal's Director-General, and its Secretary-General. ZERO-OPEN-PORT INGRESS — The core security decision: the journal is published to the internet without a single inbound port. Ingress is an outbound-only Cloudflare Tunnel — the cloudflared container dials out to Cloudflare's edge, and all public traffic rides back down that authenticated connection. No port-forwards, no exposed origin IP, no direct attack surface on a residential network. TLS terminates at Cloudflare's edge, which also provides DDoS absorption and traffic filtering for free. DEFENSE IN DEPTH BY ARCHITECTURE — The stack is three Docker containers on an isolated bridge network: a custom-built OJS 3.4 image (configuration and patches baked in at build time — the running container is reproducible, not hand-mutated), MariaDB for the editorial database, and cloudflared. The compose file publishes zero host ports: the database is reachable only by the OJS container on the internal Docker network, and OJS itself is reachable only through the tunnel. Even someone on the LAN cannot talk to the database directly. OPERATIONS — All three containers restart automatically and survive reboots unattended. Day-2 work is mine alone: image rebuilds, DNS, tunnel health monitoring, incident documentation and runbooks. The platform digitizes the journal's full academic workflow — article submission, double-blind peer review, editorial rounds, and publication — for a national legal institute, running on hardware I own.
# Zero published host ports. The journal is reachable ONLY through the tunnel.
services:
ojs:
build: ./ojs # OJS 3.4 — patches baked in at build time
depends_on:
db: { condition: service_healthy }
networks: [internal] # no route to the outside world
restart: unless-stopped
# NOTE: no `ports:` block — nothing is ever bound to the host.
db:
image: mariadb:11
environment:
MARIADB_PASSWORD_FILE: /run/secrets/db_pass # secret, never inline
secrets: [db_pass]
networks: [internal] # reachable only by the ojs container
volumes: [ojs_db:/var/lib/mysql]
restart: unless-stopped
cloudflared:
image: cloudflare/cloudflared:latest
command: tunnel --no-autoupdate run
environment:
TUNNEL_TOKEN_FILE: /run/secrets/cf_token
secrets: [cf_token]
networks: [edge, internal] # dials OUT to Cloudflare's edge
restart: unless-stopped
networks:
edge: {} # cloudflared egress only
internal: { internal: true } # air-gapped from the public internet
secrets:
db_pass: { file: ./secrets/db_pass.txt }
cf_token: { file: ./secrets/cf_token.txt }Zero published host ports — ingress is an outbound-only Cloudflare Tunnel.
