Running multiple sites from a single codebase sounds like a maintenance nightmare, but with the right architecture it can be remarkably efficient. I run 4 production video discovery sites from the same PHP codebase, each targeting different geographic regions. Here's how the architecture works at DailyWatch.
The Problem
I wanted to target different video markets (US/Europe, Asia, etc.) with separate sites for SEO and branding purposes, but I didn't want to maintain 4 separate codebases.
The Solution: Config-Driven Differentiation
The codebase is identical across all sites. Differences are controlled entirely by per-site configuration files:
// .env file - unique per site, excluded from deployment
SITE_NAME=DailyWatch
SITE_URL=https://dailywatch.video
SITE_TAGLINE=Your daily dose of trending videos
# Regions to fetch (unique per site)
FETCH_REGIONS=US,GB,DE,FR,IN,BR,AU,CA
FIXED_REGIONS=US,GB
# Cron frequency
FETCH_INTERVAL=7200
# Database (local per site)
DB_PATH=data/videos.db
Configuration Loading
class Config {
private static array $config = [];
public static function load(string $envPath = '.env'): void {
if (!file_exists($envPath)) {
throw new \RuntimeException("Config file not found: {$envPath}");
}
$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (str_starts_with(trim($line), '#')) continue;
if (!str_contains($line, '=')) continue;
[$key, $value] = explode('=', $line, 2);
self::$config[trim($key)] = trim($value);
}
}
public static function get(string $key, string $default = ''): string {
return self::$config[$key] ?? getenv($key) ?: $default;
}
public static function getArray(string $key): array {
$value = self::get($key);
return $value ? array_map('trim', explode(',', $value)) : [];
}
public static function getBool(string $key, bool $default = false): bool {
$value = self::get($key);
if ($value === '') return $default;
return in_array(strtolower($value), ['true', '1', 'yes', 'on']);
}
}
Deployment Architecture
Each site has its own hosting account with its own domain. The deploy script pushes the same code to all of them:
#!/bin/bash
# ops.sh deploy-all - Deploy to all sites in parallel
DEPLOY_HOSTS="deploy_hosts.conf"
EXCLUDE=".env,data/*,*.log,*.db"
while IFS='|' read -r host user pass path; do
[[ "$host" =~ ^#.*$ ]] && continue # Skip comments
[[ -z "$host" ]] && continue
echo "Deploying to ${host}..."
lftp -u "${user},${pass}" "${host}" <<LFTP &
set ssl:verify-certificate no
mirror --reverse --verbose --ignore-time \
--exclude-glob .env \
--exclude-glob data/ \
--exclude-glob *.log \
. ${path}
rm -rf ${path}/lscache
quit
LFTP
done < "$DEPLOY_HOSTS"
wait
echo "All deployments complete"
What Gets Shared vs. What's Per-Site
| Shared (deployed) | Per-site (excluded) |
|---|---|
| All PHP source code |
.env configuration |
| Templates and CSS |
data/ directory |
| Public assets | SQLite databases |
.htaccess |
API keys file |
composer.json |
Log files |
Handling Site-Specific Branding
Templates use config values for all site-specific text:
// In templates
<title><?= Config::get('SITE_NAME') ?> - <?= Config::get('SITE_TAGLINE') ?></title>
<link rel="canonical" href="<?= Config::get('SITE_URL') . $_SERVER['REQUEST_URI'] ?>">
<footer>
<p>© 2026 <?= Config::get('SITE_NAME') ?>. All rights reserved.</p>
</footer>
Database Per Site
Each site maintains its own SQLite database, which means:
- Different content based on different region configurations
- Independent FTS indexes
- No cross-site data dependencies
- Easy backup (just copy the .db file)
// Database path from config
$dbPath = Config::get('DB_PATH', 'data/videos.db');
$db = new PDO("sqlite:{$dbPath}");
Operational Considerations
Running 4 sites from one codebase means:
- A bug affects all sites. Test thoroughly before deploying.
- A fix reaches all sites. One deploy command updates everything.
- Feature flags let you roll out changes selectively:
// Feature flags in .env
ENABLE_BLOG=true
ENABLE_SEARCH_SUGGESTIONS=false
// In code
if (Config::getBool('ENABLE_BLOG')) {
$router->addRoute('/blog', BlogController::class);
}
This architecture powers all 4 DailyWatch sites from the same codebase. The total operational overhead is minimal — one deploy command, 4 cron configurations, and 4 .env files. Everything else is shared.
See the main site at dailywatch.video.
Top comments (0)