Skip to content
Open
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
21 changes: 21 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.git
.cursor
.fleet
.idea
.vscode
node_modules
vendor
.env
.env.*
!.env.docker.example
public/build
public/hot
bootstrap/ssr
storage/logs/*
storage/framework/cache/data/*
storage/framework/sessions/*
storage/framework/views/*
storage/debugbar/*
npm-debug.log
yarn-error.log
docker-compose.override.yml
64 changes: 64 additions & 0 deletions .env.docker.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
APP_NAME=IdeaBox
APP_ENV=production
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost:8080

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=info

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=ideabox
DB_USERNAME=ideabox
DB_PASSWORD=change-me
DB_ROOT_PASSWORD=change-root-password

BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

MEMCACHED_HOST=127.0.0.1

REDIS_CLIENT=predis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=587
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1

VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

OPENAI_API_KEY=
OPENAI_MODEL=gpt-4.1-nano
48 changes: 48 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
FROM php:8.3-cli AS php-deps

WORKDIR /app

COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer

RUN apt-get update \
&& apt-get install -y --no-install-recommends git libonig-dev libxml2-dev unzip \
&& docker-php-ext-install bcmath mbstring pdo_mysql xml \
&& rm -rf /var/lib/apt/lists/*

COPY . .

RUN composer install --no-dev --prefer-dist --optimize-autoloader --no-interaction

FROM node:20-alpine AS frontend-build

WORKDIR /app

COPY . .
COPY --from=php-deps /app/vendor ./vendor

RUN corepack enable
RUN yarn install --frozen-lockfile
RUN yarn build

FROM php:8.3-apache AS runtime

WORKDIR /var/www/html

RUN apt-get update \
&& apt-get install -y --no-install-recommends curl libonig-dev libxml2-dev \
&& docker-php-ext-install bcmath mbstring pdo_mysql opcache xml \
&& a2enmod rewrite \
&& rm -rf /var/lib/apt/lists/*

COPY . .
COPY --from=php-deps /app/vendor ./vendor
COPY --from=frontend-build /app/public/build ./public/build
COPY --from=frontend-build /app/bootstrap/ssr ./bootstrap/ssr
COPY docker/apache/000-default.conf /etc/apache2/sites-available/000-default.conf

RUN chmod +x /var/www/html/docker/start-container.sh \
&& chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 80

CMD ["/var/www/html/docker/start-container.sh"]
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,49 @@ php artisan serve

Navigate to http://localhost:8000 in your web browser to view the application.

## Docker Deployment

This repository includes a production-focused Docker setup built around `docker compose`.

1. Copy the Docker env template and fill in your production values:

```bash
cp .env.docker.example .env
```

2. Generate a stable application key and place it in `APP_KEY` inside `.env`:

```bash
docker run --rm php:8.3-cli php -r "echo 'base64:'.base64_encode(random_bytes(32)).PHP_EOL;"
```

3. Set `APP_URL` to the public HTTPS URL you will deploy behind. This is required for correct GitHub OAuth callbacks and webhook URLs.

4. Build and start the deployment stack:

```bash
docker compose up -d --build
```

5. Optionally seed the first deployment:

```bash
docker compose exec app php artisan db:seed --force
```

6. Open the application at `http://localhost:8080` locally, or at your deployed domain. Container health is exposed at `/up`.

### Updating a Deployment

Future updates follow the same rebuild-and-redeploy flow:

```bash
git pull
docker compose up -d --build
```

The app container automatically waits for MySQL, runs `php artisan migrate --force`, ensures `public/storage` is linked, and warms Laravel's config and view caches on startup.

### Contributing

Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
Expand Down
4 changes: 2 additions & 2 deletions app/Http/Controllers/Auth/AuthenticatedSessionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ public function create(): Response
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
public function store(LoginRequest $request): \Symfony\Component\HttpFoundation\Response
{
$request->authenticate();

$request->session()->regenerate();

return redirect()->intended(RouteServiceProvider::HOME);
return Inertia::location(RouteServiceProvider::HOME);
}

/**
Expand Down
27 changes: 27 additions & 0 deletions app/Http/Controllers/HealthCheckController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Throwable;

class HealthCheckController extends Controller
{
public function __invoke(): JsonResponse
{
try {
DB::connection()->getPdo();
} catch (Throwable $exception) {
return response()->json([
'status' => 'error',
], 503);
}

return response()->json([
'status' => 'ok',
]);
}
}
22 changes: 16 additions & 6 deletions database/seeders/DatabaseSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;

class DatabaseSeeder extends Seeder
{
Expand All @@ -13,12 +14,21 @@ class DatabaseSeeder extends Seeder
*/
public function run(): void
{
\App\Models\User::factory()->create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => bcrypt('password'),
'role' => 'admin',
]);
$user = User::query()->firstOrCreate(
['email' => 'admin@example.com'],
[
'name' => 'Admin User',
'password' => Hash::make('password'),
'role' => 'admin',
]
);

if ($user->name !== 'Admin User' || $user->role !== 'admin') {
$user->forceFill([
'name' => 'Admin User',
'role' => 'admin',
])->save();
}

$this->call([
StatusSeeder::class,
Expand Down
43 changes: 43 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
image: ideabox:latest
restart: unless-stopped
env_file:
- .env
depends_on:
db:
condition: service_healthy
ports:
- "8080:80"
volumes:
- app_storage:/var/www/html/storage
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1/up >/dev/null || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 30s

db:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_DATABASE: ${DB_DATABASE}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s

volumes:
db_data:
app_storage:
13 changes: 13 additions & 0 deletions docker/apache/000-default.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<VirtualHost *:80>
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html/public

<Directory /var/www/html/public>
AllowOverride All
Require all granted
Options FollowSymLinks
</Directory>

ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
64 changes: 64 additions & 0 deletions docker/start-container.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/bin/sh

set -eu

cd /var/www/html

if [ -z "${APP_KEY:-}" ]; then
echo "APP_KEY is not set. Copy .env.docker.example to .env and add a stable APP_KEY before starting the stack." >&2
exit 1
fi

mkdir -p \
bootstrap/cache \
storage/app/public \
storage/debugbar \
storage/framework/cache/data \
storage/framework/sessions \
storage/framework/testing \
storage/framework/views \
storage/logs

chown -R www-data:www-data storage bootstrap/cache
chmod -R ug+rwx storage bootstrap/cache

if [ "${DB_CONNECTION:-mysql}" = "mysql" ]; then
echo "Waiting for MySQL at ${DB_HOST:-db}:${DB_PORT:-3306}..."

php <<'PHP'
<?php
$host = getenv('DB_HOST') ?: 'db';
$port = (int) (getenv('DB_PORT') ?: 3306);
$database = getenv('DB_DATABASE') ?: 'ideabox';
$username = getenv('DB_USERNAME') ?: 'root';
$password = getenv('DB_PASSWORD') ?: '';
$attempts = 30;
for ($attempt = 1; $attempt <= $attempts; $attempt++) {
try {
$dsn = sprintf('mysql:host=%s;port=%d;dbname=%s', $host, $port, $database);
new PDO($dsn, $username, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
exit(0);
} catch (Throwable $exception) {
fwrite(STDERR, sprintf("MySQL not ready (%d/%d): %s\n", $attempt, $attempts, $exception->getMessage()));
sleep(2);
}
}
fwrite(STDERR, "MySQL did not become ready in time.\n");
exit(1);
PHP
fi

php artisan migrate --force

if [ ! -e public/storage ]; then
php artisan storage:link
fi

php artisan config:clear >/dev/null 2>&1 || true
php artisan view:clear >/dev/null 2>&1 || true
php artisan config:cache
php artisan view:cache

exec apache2-foreground
Loading