DreamHost shared hosting: how it works
This page describes a DreamHost shared hosting account as its owner sees it: the login dh_user, the server <server>.dreamhost.com, the document root ~/example.com/ and the site example.com. The difference from Beget is not the brand but the web layer, and the next chapters show where it bites. Tools used here: curl, SSH, grep.
Apache alone, TLS in Apache
There is no nginx in front. Apache accepts the connection and, for an https address, Apache itself holds the certificate. The response header says only Apache:
curl -sS -D - -o /dev/null https://example.com/
HTTP/2 200 content-type: text/html; charset=UTF-8 server: Apache
The probe from the server check gives the details. Write it into the document root under a name nobody guesses:
vi ~/example.com/probe-7f3a.php
<?php
echo "HTTPS=", var_export($_SERVER['HTTPS'] ?? null, true),
" XFP=", var_export($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null, true),
" SERVER_SOFTWARE=", $_SERVER['SERVER_SOFTWARE'] ?? '',
" sapi=", PHP_SAPI, "\n";
curl -sS https://example.com/probe-7f3a.php
HTTPS='on' XFP=NULL SERVER_SOFTWARE=Apache sapi=cgi-fcgi
HTTPS='on' means TLS ends in this Apache. No proxy sends X-Forwarded-Proto, so XFP=NULL. An https redirect that tests only for that header fires on https requests too and sends every address to itself. A rule that works here tests %{HTTPS}:
RewriteCond %{HTTPS} off RewriteCond %{HTTP:X-Forwarded-Proto} !https RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
The loop this rule fixed, with the commands that show it, is in Move to DreamHost: the loop. Delete the probe when done:
rm ~/example.com/probe-7f3a.php
Static files go through .htaccess
With no front server, Apache also serves images, CSS and JavaScript, so every rule in .htaccess runs for them too. On Beget it is the other way round: nginx answers a static file that exists on disk, and Apache never reads the rules for it (nginx in front of Apache). Two things follow on DreamHost:
- A redirect for a moved image works even while the old file is still on the server.
- A rule written for pages that also matches a file path now runs for that file. On Beget such a rule stays hidden, and it shows up only after the move.
A real case. The site keeps its PHP classes, and the images and scripts they use, in a folder named .php. Its .htaccess sends a PHP page with PATH_INFO (page.php/extra) to the clean address:
RewriteCond %{REQUEST_FILENAME} -f RewriteCond %{REQUEST_URI} ^(.+\.php)/.+$ RewriteRule ^ https://%{HTTP_HOST}%1 [R=301,L]
The pattern also matches /.php/img/..., because the folder name ends in .php and a slash follows. On DreamHost an image in that folder answers 301, and the redirect points at the folder:
curl -sS -o /dev/null -D - https://example.com/.php/img/cards/airport.jpg | grep -i -E '^(HTTP|location|server)'
HTTP/2 301 location: https://example.com/.php server: Apache
The page shows a broken image, and nothing in the error log explains it. The access log shows which files were hit, scripts too:
awk '$9 == 301 && $7 ~ /\/\.php\// {print $7}' ~/logs/example.com/https/access.log | sort | uniq -c
3 /ru/.php/img/cards/actress-800x600.jpg 1 /.php/scripts/top10_script3.js 1 /.php/scripts/top10_script2.js 1 /ao/.php/scripts/top10_script2.js
The fix is to require a file name before .php:
RewriteCond %{REQUEST_FILENAME} -f RewriteCond %{REQUEST_URI} ^(.*[^/]\.php)/.+$ RewriteRule ^ https://%{HTTP_HOST}%1 [R=301,L]
[^/] is any character except a slash. So page.php/extra still matches, and /.php/img/cards/airport.jpg does not. The same request against a Beget-like and a DreamHost-like server, before and after the fix, is test 6 of the Docker test plan.
PHP as FastCGI
DreamHost runs PHP as a FastCGI application on every hosting plan (PHP overview). On a shared server that means mod_fcgid starts a worker per site; the worker binary is named after the PHP version, for example php85.cgi for PHP 8.5. The probe above answered sapi=cgi-fcgi, and two commands show the same thing from the shell:
pgrep -l -u user php
2745746 php85.cgi
ls /usr/sbin/apache2*
/usr/sbin/apache2 /usr/sbin/apache2ctl
Apache is installed, but there is no php_module in its module list: PHP is not inside it. The full check, including apache2ctl -M on a server you control, is in Which server do you have.
Why php_flag answers 500
php_flag and php_value are directives of mod_php. On DreamHost no mod_php is loaded, so Apache does not know the words. One such line in .htaccess stops every request in that folder and below it. The site answers 500 on every page, the home page included:
curl -sS -D - -o /dev/null https://example.com/
HTTP/2 500 content-length: 533 content-type: text/html; charset=iso-8859-1 server: Apache
The error log names the file and the first unknown directive:
tail -n 1 ~/logs/example.com/https/error.log
[Fri Nov 06 23:29:54 2026] [example.com] [alert] [client 203.0.113.7:37118] [pid 3064457] config.c(2045): /home/user/example.com/.htaccess: Invalid command 'php_flag', perhaps misspelled or defined by a module not included in the server configuration
The lines that caused it, from the same account:
grep -n -E '^\s*php_(flag|value)' ~/example.com/.htaccess
78:php_flag display_startup_errors off 79:php_flag display_errors off 80:php_flag html_errors off 81:php_flag log_errors on 82:php_flag ignore_repeated_errors off 83:php_flag ignore_repeated_source off 84:php_flag report_memleaks on 85:php_flag track_errors on 86:php_value docref_root 0 87:php_value docref_ext 0 91:php_value error_reporting -1 92:php_value log_errors_max_len 0
Guard the block so that a host without mod_php skips it instead of failing:
vi ~/example.com/.htaccess
<IfModule mod_php.c> php_flag display_errors off php_flag log_errors on # the other ten lines stay inside this block </IfModule>
The complete twelve-line block and the rollback copy are in 500 after a host move: the fix. The general form of the guard is in IfModule.
PHP settings in phprc
With the block skipped, PHP runs with the host defaults. DreamHost reads extra settings per PHP version from a file in the home folder, named phprc, which works like php.ini (php.ini overview). For PHP 8.5 the path is ~/.php/8.5/phprc:
vi ~/.php/8.5/phprc
display_startup_errors = Off display_errors = Off html_errors = Off log_errors = On ignore_repeated_errors = Off ignore_repeated_source = Off report_memleaks = On error_reporting = -1
track_errors was removed in PHP 8.0 and log_errors_max_len in PHP 8.1, so they stay out. PHP reads the file when its worker starts. End the running worker so the next request starts a new one:
pkill -x php85.cgi
The provider gives the same instruction with killall -9 php85.cgi -u dh_user and explains why the file may seem to do nothing (My phprc file isn't updating). The probe after the reload shows the new values; the full before and after output is in the php_flag case.
Error and access logs
Logs live in the home folder, one folder per domain and one per protocol. For the https site of this example:
~/logs/example.com/https/error.log
~/logs/example.com/https/access.log
The http site has the same pair under ~/logs/example.com/http/. Apache writes its own errors there, and PHP messages follow when error_log is empty, marked mod_fcgid: stderr:
[Sat Nov 07 01:01:52 2026] [example.com] [warn] [client 203.0.113.9:61816] [pid 1836802] fcgid_bucket.c(153): mod_fcgid: stderr: PHP Deprecated: Creation of dynamic property Related::$urls is deprecated in /home/user/example.com/.php/Related.php on line 10
The provider documents the same files and the panel viewer (Viewing and examining your error log via SSH). A 500 from a broken directive is a config.c line, as above; a 403 from a mode is a Permission denied line.
SSH and file modes
SSH is included, but the website user must be a shell user (SSH overview). Log in with the key you uploaded, then go to the document root:
ssh -i ~/.ssh/<key> dh_user@<server>.dreamhost.com
cd ~/example.com
The account works with the usual UNIX modes: folders 755, files 644. Apache runs as another user, so a folder 700 or a file 600 is invisible to it and the page answers 403, or 404 when the whole folder is closed. The provider describes the bits in Unix permissions overview. Run from the document root after an upload, the owner is dh_user and the modes are:
stat -c '%a %U:%G %n' dev/python dev/python/files
755 dh_user:pg000000 dev/python 700 dh_user:pg000000 dev/python/files
rsync and zip keep those modes, and the new server copies them faithfully. Save the old modes to a file, then fix folders and files on the server and in the local checkout:
find ~/example.com -printf '%m %p\n' > ~/backups/example.com.modes.$(date +%Y%m%d-%H%M%S)
find ~/example.com -type d -exec chmod 755 {} +
find ~/example.com -type f -exec chmod 644 {} +
The next upload must not bring the modes back. Copy with rsync --chmod=D755,F644; the option is explained in Copy the files. The move that hit this problem is written up in the example, and the Docker image reproduces it with a read-only document root owned by another user: test 5.
Python and Node.js
DreamHost documents Python as CGI and FastCGI scripts on shared hosting. A Python file must end in .py, .cgi or .fcgi, start with the path of the interpreter, have mode 755 and use UNIX line endings (Guidelines for setting up a Python file). Python 3 is installed; a custom version and packages go into a virtual environment under the home folder (Python overview).
Passenger is not a runtime on DreamHost: the provider's Supported and unsupported technologies page lists Passenger as NO, not supported on DreamHost servers. A WSGI framework application therefore has no Passenger entry point here, unlike on Beget.
Node.js is available on Managed VPS and Dedicated servers, not on shared hosting, as the same supported technologies page and the Node.js overview describe it.
Test against a DreamHost-like server
The repository has a Docker image that copies this web layer: Apache alone, PHP as FastCGI through mod_fcgid, no mod_php, a per-site phprc folder, logs under /logs/example.com/ and a self-signed certificate. Run your site there before the move: The DreamHost-like image.
Article author: Arthur Isaev