Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules
vendor
.git
.env
.env.*
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
tests
.github
.claude
docker/production/docker-compose.prod.yml
75 changes: 75 additions & 0 deletions app/Console/Commands/SwordInit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace App\Console\Commands;

use App\Models\Server;
use App\Models\User;
use Illuminate\Console\Command;

class SwordInit extends Command
{
protected $signature = 'sword:init {config-file : Path to JSON config file with init parameters}';

protected $description = 'Initialize SWORD with an admin user and localhost server';

public function handle(): int
{
$configPath = $this->argument('config-file');

if (! file_exists($configPath)) {
$this->error("Config file not found: {$configPath}");

return self::FAILURE;
}

$config = json_decode(file_get_contents($configPath), true);

if (json_last_error() !== JSON_ERROR_NONE) {
$this->error('Invalid JSON in config file: '.json_last_error_msg());

return self::FAILURE;
}

$required = ['admin_name', 'admin_email', 'admin_password', 'server_ip', 'mysql_root_password', 'sudo_password', 'ssh_private_key', 'ssh_public_key'];
foreach ($required as $key) {
if (empty($config[$key])) {
$this->error("Missing required config key: {$key}");

return self::FAILURE;
}
}

$user = User::firstOrCreate(
['email' => $config['admin_email']],
[
'name' => $config['admin_name'],
'password' => $config['admin_password'],
],
);

$this->info("Admin user ready: {$user->email}");

$server = Server::firstOrCreate(
['provider' => 'localhost'],
[
'user_id' => $user->id,
'name' => 'Localhost',
'ip_address' => $config['server_ip'],
'hostname' => gethostname(),
'timezone' => date_default_timezone_get(),
'ssh_port' => 22,
'ssh_private_key' => $config['ssh_private_key'],
'ssh_public_key' => $config['ssh_public_key'],
'mysql_root_password' => $config['mysql_root_password'],
'sudo_password' => $config['sudo_password'],
'status' => 'provisioned',
'provisioned_at' => now(),
'is_online' => true,
],
);

$this->info("Localhost server ready: {$server->ip_address} (ID: {$server->id})");

return self::SUCCESS;
}
}
123 changes: 123 additions & 0 deletions docker/production/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# ── Stage 1: Install PHP dependencies ───────────────────
FROM ubuntu:24.04 AS php-deps

ENV DEBIAN_FRONTEND=noninteractive

WORKDIR /app

RUN apt-get update \
&& mkdir -p /etc/apt/keyrings \
&& apt-get install -y gnupg curl ca-certificates zip unzip \
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
&& apt-get update \
&& apt-get install -y \
php8.5-cli \
php8.5-mysql \
php8.5-gd \
php8.5-curl \
php8.5-mbstring \
php8.5-xml \
php8.5-zip \
php8.5-bcmath \
php8.5-intl \
php8.5-readline \
php8.5-redis \
php8.5-igbinary \
php8.5-msgpack \
php8.5-soap \
php8.5-ldap \
php8.5-imagick \
php8.5-sqlite3 \
&& curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \
&& rm -rf /var/lib/apt/lists/*

COPY composer.json composer.lock ./
RUN composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader --no-scripts

COPY . .
RUN composer dump-autoload --optimize

# ── Stage 2: Build frontend assets ─────────────────────
FROM php-deps AS assets

RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main" > /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*

RUN npm ci
RUN npm run build

# ── Stage 3: Production image ──────────────────────────
FROM ubuntu:24.04

LABEL maintainer="Synio"

ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=UTC
ENV LANG=C.UTF-8

WORKDIR /var/www/html

RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom

RUN apt-get update && apt-get upgrade -y \
&& mkdir -p /etc/apt/keyrings \
&& apt-get install -y gnupg curl ca-certificates zip unzip supervisor nginx \
&& curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xb8dc7e53946656efbce4c1dd71daeaab4ad4cab6' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \
&& echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu noble main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \
&& apt-get update \
&& apt-get install -y \
php8.5-fpm \
php8.5-cli \
php8.5-mysql \
php8.5-gd \
php8.5-curl \
php8.5-mbstring \
php8.5-xml \
php8.5-zip \
php8.5-bcmath \
php8.5-intl \
php8.5-readline \
php8.5-redis \
php8.5-igbinary \
php8.5-msgpack \
php8.5-soap \
php8.5-ldap \
php8.5-imagick \
php8.5-sqlite3 \
&& apt-get -y autoremove \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# Remove default nginx site
RUN rm -f /etc/nginx/sites-enabled/default

# Copy configs
COPY docker/production/nginx.conf /etc/nginx/sites-enabled/default
COPY docker/production/php-fpm.conf /etc/php/8.5/fpm/pool.d/www.conf
COPY docker/production/supervisord.conf /etc/supervisor/conf.d/supervisord.conf

# Copy application with vendor from php-deps stage
COPY --from=php-deps --chown=www-data:www-data /app /var/www/html

# Copy compiled frontend assets from assets stage
COPY --from=assets --chown=www-data:www-data /app/public/build /var/www/html/public/build

# Ensure storage and cache dirs exist with correct permissions
RUN mkdir -p storage/logs storage/framework/cache storage/framework/sessions storage/framework/views bootstrap/cache \
&& chown -R www-data:www-data storage bootstrap/cache \
&& chmod -R 775 storage bootstrap/cache

# Create directory for PHP-FPM socket/pid
RUN mkdir -p /run/php

EXPOSE 8080

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
29 changes: 29 additions & 0 deletions docker/production/docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
services:
app:
image: sword-app:latest
container_name: sword_app
restart: unless-stopped
env_file: /srv/sword/app/.env
volumes:
- /srv/sword/app/storage:/var/www/html/storage
networks:
- sword_network
labels:
- traefik.enable=true
- traefik.http.routers.sword.rule=Host(`${SWORD_DOMAIN}`)
- traefik.http.routers.sword.entrypoints=websecure
- traefik.http.routers.sword.tls.certresolver=letsencrypt
- traefik.http.services.sword.loadbalancer.server.port=8080

redis:
image: redis:alpine
container_name: sword_redis
restart: unless-stopped
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}"]
networks:
- sword_network

networks:
sword_network:
name: sword_network
external: true
34 changes: 34 additions & 0 deletions docker/production/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
server {
listen 8080 default_server;
server_name _;
root /var/www/html/public;

index index.php;

charset utf-8;

client_max_body_size 100M;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }

error_page 404 /index.php;

location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_param HTTPS on;
fastcgi_param HTTP_X_FORWARDED_PROTO $http_x_forwarded_proto;
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
fastcgi_param HTTP_X_FORWARDED_HOST $http_x_forwarded_host;
}

location ~ /\.(?!well-known).* {
deny all;
}
}
14 changes: 14 additions & 0 deletions docker/production/php-fpm.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[www]
user = www-data
group = www-data

listen = 127.0.0.1:9000

pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 2
pm.max_spare_servers = 10
pm.max_requests = 500

clear_env = no
44 changes: 44 additions & 0 deletions docker/production/supervisord.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid

[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:php-fpm]
command=/usr/sbin/php-fpm8.5 -F
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

[program:queue-worker]
command=/usr/bin/php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600
user=www-data
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
numprocs=1
stdout_logfile=/var/www/html/storage/logs/queue-worker.log
stderr_logfile=/var/www/html/storage/logs/queue-worker.log

[program:scheduler]
command=/usr/bin/php /var/www/html/artisan schedule:work
user=www-data
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
stdout_logfile=/var/www/html/storage/logs/scheduler.log
stderr_logfile=/var/www/html/storage/logs/scheduler.log
Loading
Loading